// Copyright 2021 The Dawn Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "src/dawn_node/binding/GPUBuffer.h"

#include <memory>

#include "src/dawn_node/binding/Converter.h"
#include "src/dawn_node/binding/Errors.h"
#include "src/dawn_node/utils/Debug.h"

namespace wgpu::binding {

    ////////////////////////////////////////////////////////////////////////////////
    // wgpu::bindings::GPUBuffer
    // TODO(crbug.com/dawn/1134): We may be doing more validation here than necessary. Once CTS is
    // robustly passing, pull out validation and see what / if breaks.
    ////////////////////////////////////////////////////////////////////////////////
    GPUBuffer::GPUBuffer(wgpu::Buffer buffer,
                         wgpu::BufferDescriptor desc,
                         wgpu::Device device,
                         std::shared_ptr<AsyncRunner> async)
        : buffer_(std::move(buffer)),
          desc_(desc),
          device_(std::move(device)),
          async_(std::move(async)) {
        if (desc.mappedAtCreation) {
            state_ = State::MappedAtCreation;
        }
    }

    interop::Promise<void> GPUBuffer::mapAsync(Napi::Env env,
                                               interop::GPUMapModeFlags mode,
                                               interop::GPUSize64 offset,
                                               std::optional<interop::GPUSize64> size) {
        wgpu::MapMode md{};
        Converter conv(env);
        if (!conv(md, mode)) {
            interop::Promise<void> promise(env, PROMISE_INFO);
            promise.Reject(Errors::OperationError(env));
            return promise;
        }

        if (state_ != State::Unmapped) {
            interop::Promise<void> promise(env, PROMISE_INFO);
            promise.Reject(Errors::OperationError(env));
            device_.InjectError(wgpu::ErrorType::Validation,
                                "mapAsync called on buffer that is not in the unmapped state");
            return promise;
        }

        struct Context {
            Napi::Env env;
            interop::Promise<void> promise;
            AsyncTask task;
            State& state;
        };
        auto ctx = new Context{env, interop::Promise<void>(env, PROMISE_INFO), async_, state_};
        auto promise = ctx->promise;

        uint64_t s = size.has_value() ? size.value() : (desc_.size - offset);

        state_ = State::MappingPending;

        buffer_.MapAsync(
            md, offset, s,
            [](WGPUBufferMapAsyncStatus status, void* userdata) {
                auto c = std::unique_ptr<Context>(static_cast<Context*>(userdata));
                c->state = State::Unmapped;
                switch (status) {
                    case WGPUBufferMapAsyncStatus_Force32:
                        UNREACHABLE("WGPUBufferMapAsyncStatus_Force32");
                        break;
                    case WGPUBufferMapAsyncStatus_Success:
                        c->promise.Resolve();
                        c->state = State::Mapped;
                        break;
                    case WGPUBufferMapAsyncStatus_Error:
                        c->promise.Reject(Errors::OperationError(c->env));
                        break;
                    case WGPUBufferMapAsyncStatus_UnmappedBeforeCallback:
                    case WGPUBufferMapAsyncStatus_DestroyedBeforeCallback:
                        c->promise.Reject(Errors::AbortError(c->env));
                        break;
                    case WGPUBufferMapAsyncStatus_Unknown:
                    case WGPUBufferMapAsyncStatus_DeviceLost:
                        // TODO: The spec is a bit vague around what the promise should do
                        // here.
                        c->promise.Reject(Errors::UnknownError(c->env));
                        break;
                }
            },
            ctx);

        return promise;
    }

    interop::ArrayBuffer GPUBuffer::getMappedRange(Napi::Env env,
                                                   interop::GPUSize64 offset,
                                                   std::optional<interop::GPUSize64> size) {
        if (state_ != State::Mapped && state_ != State::MappedAtCreation) {
            Errors::OperationError(env).ThrowAsJavaScriptException();
            return {};
        }

        uint64_t s = size.has_value() ? size.value() : (desc_.size - offset);

        uint64_t start = offset;
        uint64_t end = offset + s;
        for (auto& mapping : mapped_) {
            if (mapping.Intersects(start, end)) {
                Errors::OperationError(env).ThrowAsJavaScriptException();
                return {};
            }
        }

        auto* ptr = (desc_.usage & wgpu::BufferUsage::MapWrite)
                        ? buffer_.GetMappedRange(offset, s)
                        : const_cast<void*>(buffer_.GetConstMappedRange(offset, s));
        if (!ptr) {
            Errors::OperationError(env).ThrowAsJavaScriptException();
            return {};
        }
        auto array_buffer = Napi::ArrayBuffer::New(env, ptr, s);
        // TODO(crbug.com/dawn/1135): Ownership here is the wrong way around.
        mapped_.emplace_back(Mapping{start, end, Napi::Persistent(array_buffer)});
        return array_buffer;
    }

    void GPUBuffer::unmap(Napi::Env env) {
        if (state_ == State::Destroyed) {
            device_.InjectError(wgpu::ErrorType::Validation,
                                "unmap() called on a destroyed buffer");
            return;
        }

        for (auto& mapping : mapped_) {
            mapping.buffer.Value().Detach();
        }
        mapped_.clear();
        buffer_.Unmap();
        state_ = State::Unmapped;
    }

    void GPUBuffer::destroy(Napi::Env) {
        buffer_.Destroy();
        state_ = State::Destroyed;
    }

    std::optional<std::string> GPUBuffer::getLabel(Napi::Env) {
        UNIMPLEMENTED();
    }

    void GPUBuffer::setLabel(Napi::Env, std::optional<std::string> value) {
        UNIMPLEMENTED();
    }

}  // namespace wgpu::binding
